\clearpage

\section{项目功能以及性能测试}

为了给设计的 WebSocket 框架进行测试，附录中的代码片段 \ref{websocket-server-example} 给出了一个简单的基于框架的 WebSocket 服务器实例，用户通过标准输入来控制服务器向客户端发送数据。对于接收到的 WebSocket 完整帧，服务器会在日志中记录帧的类型、长度和数据采样，对于二进制数据则会将其写入文件。日志通过设置会同时在标准输出和日志文件中输出。WebSocket 客户端的实现如代码片段 \ref{websocket-client-example}，可以通过标准输入来向服务器发送文本数据和二进制文件。

项目使用 CMake 与 Visual Studio 2017 作为构建工具。将 Release 二进制文件编译到 \texttt{build} 文件夹可以在项目根目录使用如 \ref{cmake-build} 中的命令进行构建，生成得到相应的 WebSocket 服务器和客户端的库文件以及作为示例的两个可执行文件。

\begin{lstlisting}[
  captionpos=b,
  caption={CMake 构建命令}, 
  label={cmake-build}
]
cmake --build ./build --config Release --target all -- -j 10
\end{lstlisting}

日志系统包含了精确到毫秒的时间戳，同时为 WebSocket 协议的各种行为都提供了不同等级的详尽日志。用户可以设置日志等级宏在编译期过滤掉不需要的日志输出。服务器输出的日志格式如图 \ref{ws-server-log-example} 所示。

\begin{figure}[htbp]
  \centering
  \includegraphics[width=\linewidth]{final/ws-server-log-example.png}
  \caption{服务器日志示例}
  \label{ws-server-log-example}
\end{figure}

在 \texttt{websocket\_frame} 类中实现了一个摘要方法用来描述帧的内容，其输出格式类似代码片段 \ref{websocket-frame-description}。data sample 中会截取前 100 个字节的数据，对于除文本以外的内容会输出其 16 进制下的表示，否则输出文本。

\begin{lstlisting}[
  captionpos=b,
  caption={WebSocket 帧描述格式}, 
  label={websocket-frame-description}
]
type: pong
length: 8
data sample: 50 d1 6f 25 72 01 00 00
\end{lstlisting}
\subsection{文件传输测试}

本机测试环境如表 \ref{local-test-environment} 所示。

\begin{table}[ht]
  \caption{\label{local-test-environment} 本机测试环境 }
  \begin{tabularx}{\linewidth}{c|X<{\centering}}
    \hline
    CPU      & Intel(R) Core(TM) i7-6700HQ 2.6GHz                        \\ \hline
    磁盘     & SanDisk SD8SN8U 512G 1122                                 \\ \hline
    操作系统 & Windows 10 64位 专业版 18363.778                          \\ \hline
    RAM      & 16 GB                                                     \\ \hline
    编译器   & 用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.16.27035 版 \\ \hline
  \end{tabularx}
\end{table}

WebSocket 客户端的示例可执行文件提供了发送文本和发送二进制文件的功能，可以通过发送较大的文件来对 WebSocket 服务器的吞吐量进行简单的估计。

\par

测试过程中，客户端会读取磁盘中一个大小为 526400992 字节的文件，调用 WebSocket 客户端的数据发送接口，其将文件分割为大小为 1 MB 的块装入 WebSocket 帧发送给服务器。服务器接收后将数据解析为 WebSocket 帧，并最终拼装为完整的二进制文件写入磁盘。下面是一次测试结果。


\begin{itemize}[topsep=5pt]
  \setlength{\itemindent}{0pt}
        \setlength{\itemsep}{0pt}
        \setlength{\parsep}{0pt}
        \setlength{\parskip}{0pt}
  \item 客户端读取文件并发送（图 \ref{client-send-file}）
        \begin{figure}[htbp]
          \centering
          \includegraphics[width=.9\linewidth]{final/client-send-file.png}
          \caption{客户端读取文件并发送}
          \label{client-send-file}
        \end{figure}
  \item 服务器接收到第一组数据（图 \ref{server-recv-file-start}）
        \begin{figure}[htbp]
          \centering
          \includegraphics[width=.9\linewidth]{final/server-recv-file-start.png}
          \caption{服务器收到第一组数据}
          \label{server-recv-file-start}
        \end{figure}
  \item 服务器收到最后一组数据并拼装为完整的数据帧，将帧摘要输入日志（图 \ref{server-recv-file-end}）
        \begin{figure}[htbp]
          \centering
          \includegraphics[width=.9\linewidth]{final/server-recv-file-end.png}
          \caption{服务器收到最后一组数据}
          \label{server-recv-file-end}
        \end{figure}
\end{itemize}

注意到，客户端开始向服务器发送数据的本地时间是 2020 年 5 月 18 日，14 点 15 分 18 秒 951 毫秒，服务器开始接收数据为 18 秒 963 毫秒，接收完毕并拼装结束在 22 秒 194 毫秒。

经过简单的计算可以得出，本次文件传输的平均速率约为 154.8 MB / s （计算过程如 \ref{single-speed}）。

\begin{equation}
  V_{\textmd{average}} = \frac{\texttt{sizeof}\,\, \textmd{test.bin}}{\textmd{time}}
  = \frac{526400992\, \textmd{byte}}{3.231\, \textmd{second}}
  = 154.7996\, (\textmd{MB} / \textmd{second})
  \label{single-speed}
\end{equation}

但实际上上述性能是在服务器输出了 debug 级别的日志信息，鉴于朴素的日志实现，可能会在多线程竞争以及 I/O 上耗费更多的时间。通过在 \texttt{logdef.hpp} 中修改日志的输出等级，以及禁用日志文件输出，再次测试得到的结果如 \ref{file-test-result} 所示。计算得到，最终的文件传输速率为 181.824 MB / s。观察进程监视器可以发现，限制传输速率的主要因素在此场景下为磁盘 I/O，CPU 占用始终未超过 10\%。

\begin{lstlisting}[
  captionpos=b,
  caption={文件传输测试结果},
  label={file-test-result}
]
// client 
> file misc/test.bin
[2020-05-19 10:20:57.810][17420][..\src\client\client_main.cpp:82][info] send file start

// server
[2020-05-19 10:21:00.571][19848][..\src\server\websocket_server.cpp:36][info] received binary frame from 
client 127.0.0.1:49854 with size 526400992
\end{lstlisting}

\subsection{并发压力测试}

我们使用了 Artillery (\url{https://artillery.io/}) 作为并发压力测试的工具。Artillery 是一个基于 Node.js 的现代化的压测工具，使用简单的脚本即可定制不同规格、针对不同协议的压力测试。

我们设置的压测内容为：每秒压测工具会向服务器开启 300 个连接（超过 300 个连接本机 CPU 不足以支撑），每个连接向服务器发送 1KB 的文本数据，等待 5 秒钟，随后再向发送 1KB 的数据重复 4 次，再关闭连接。压测过程（至少）持续 10 秒，测试结果如代码片段 \ref{artillery-test-result} 所示。

\begin{lstlisting}[
  captionpos=b,
  caption={Artillery 测试结果},
  label={artillery-test-result}
]
> artillery run .\test\loadtest.yml

All virtual users finished
Summary report @ 19:50:39(+0800) 2020-05-19
  Scenarios launched:  3000
  Scenarios completed: 1689
  Requests completed:  6756
  Mean response/sec: 509.89
  Response time (msec):
    min: 0
    max: 3.7
    median: 0.1
    p95: 0.2
    p99: 0.4
  Scenario counts:
    0: 3000 (100%)
  Codes:
    0: 6756
  Errors:
    connect ECONNREFUSED 127.0.0.1:1234: 1311
\end{lstlisting}

可以看到，我们的服务器在面对大量请求时仍然能够提供非常良好的响应速度，99\% 的请求都在 0.8 毫秒内得到了响应，而中位数只有 0.1 毫秒。尽管在本地的测试不需要经过网卡，但也能够部分说明本框架在高负载场景下的性能。

\subsection{与同类产品的对比}

WebSocket 在当今已经用途广泛，基于 C++ 实现的框架也有很多成熟的产品，例如 WebSocket++、Boost.Beast、\(\mu\) Websocket 等，这里选出较为流行的两款与本框架进行对比。

\subsubsection{WebSocket++}

WebSocket++ 是一款基于 C++11 和 Boost.Asio 的跨平台 WebSocket Header Only 实现，并基于 BSD 协议开源。基于 WebSocket++ 提供的样例，我们也实现了一个简短的 WebSocket 服务器，并测试其文件传输效率。\texttt{websocketpp::server} 设置的默认最大整帧大小为 32 MB，在后续测试中会出现错误，需要手动设置 \texttt{max\_message\_size} 来扩大限制。

WebSocket++ 的使用也非常简洁，代码片段 \ref{websocketpp-server-example} 是一个最简形式的服务器实现，仅提供了输出收到的 WebSocket 帧头部和大小的功能。我们将使用这个服务器来对
其吞吐量进行估计。

代码使用 MSVC 19.16.27035 以 Release 模式在 x64 环境下，基于 Boost 、Boost.Date\_Time、Boost.Regex 的 1.72.0.0 版本进行编译。使用本设计实现的客户端发起连接并发送大小为 526400992 字节大小的二进制 WebSocket 帧，利用客户端读取完文件开始发送到服务器整合完 WebSocket 帧并调用回调函数之间的差值作为文件传输的时间，测试结果如 \ref{websocketpp-file-test-result} 所示，可以求得传输时间为 2.892 秒，计算得到传输速率为 173.58 MB/s ，和本项目实现的服务器相差无几，主要瓶颈也是在 I/O 上。

\begin{lstlisting}[
  captionpos=b,
  caption={WebSocket++ 文件传输测试结果},
  label={websocketpp-file-test-result}
]
// client 
> file misc/test.bin
[2020-05-19 14:48:11.417][19356][..\src\client\client_main.cpp:82][info] send file start

// server
[2020-05-19 14:48:08] [connect] WebSocket Connection [::ffff:127.0.0.1]:65259 v13 "" / 101
[2020-05-19 14:48:13] [control] Control frame received with opcode 9
[2020-05-19 14:48:13] [frame_header] Dispatching write containing 1 message(s) containing 2 header bytes and 8 payload bytes
[2020-05-19 14:48:13] [frame_header] Header Bytes:
[0] (2) 8A 08

[2020-05-19 14:48:14.309] received frame with size 526400992
\end{lstlisting}

我们再对此生成的二进制文件使用 Artillery 进行压力测试，同样是持续 10 秒、每秒 300 个线程连接并发送 1 KB 数据，等待 5 秒并重复 4 次，结果如 \ref{websocketpp-loadtest-result} 所示。

\begin{lstlisting}[
  captionpos=b,
  caption={WebSocket++ 服务器压力测试结果},
  label={websocketpp-loadtest-result}
]
All virtual users finished
Summary report @ 15:21:21(+0800) 2020-05-19
  Scenarios launched:  3000
  Scenarios completed: 686
  Requests completed:  2744
  Mean response/sec: 165.6
  Response time (msec):
    min: 0
    max: 3.4
    median: 0.1
    p95: 0.2
    p99: 0.4
  Scenario counts:
    0: 3000 (100%)
  Codes:
    0: 2744
  Errors:
    connect ECONNREFUSED 127.0.0.1:1234: 2314
\end{lstlisting}

\subsubsection{Boost.Beast}

Boost.Beast 是 Boost 中的一个子项目，是一个跨平台的 HTTP 和 WebSocket 协议 C++11 Header Only 实现。相较于 WebSocket++，Boost.Beast 提供了对于同步 IO和类似于 \texttt{std::future} 的有栈/无栈协程的支持，并利用 ASIO 的性质尽量避免了互斥量的使用。

Boost.Beast 的性质决定了实现的 服务器不会非常简短，这里也不再放上示例服务器代码。对于文件传送的测试依然和上方两次相同，测试结果如 \ref{beast-file-test-result} 所示。数据传输的时间为 2.515 秒，计算可得传输速率为 199.61 MB/s。

\begin{lstlisting}[
  captionpos=b,
  caption={Boost.Beast 文件传输测试结果},
  label={beast-file-test-result}
]
// client 
> file misc/test.bin
[2020-05-19 19:31:10.937][12884][..\src\client\client_main.cpp:82][info] send file start

// server
[2020-May-19 19:31:13.452339] received 526400992 bytes
\end{lstlisting}

同样的，我们也使用 Artillery 进行了相同的并发压力测试，结果如 \ref{beast-loadtest-result} 所示。

\begin{lstlisting}[
  captionpos=b,
  caption={Boost.Beast 服务器压力测试结果},
  label={beast-loadtest-result}
]
All virtual users finished
Summary report @ 19:47:52(+0800) 2020-05-19
  Scenarios launched:  3000
  Scenarios completed: 882
  Requests completed:  3528
  Mean response/sec: 233.64
  Response time (msec):
    min: 0
    max: 3.7
    median: 0.1
    p95: 0.2
    p99: 0.4
  Scenario counts:
    0: 3000 (100%)
  Codes:
    0: 3528
  Errors:
    connect ECONNREFUSED 127.0.0.1:1234: 2118
\end{lstlisting}

\subsubsection{框架之间的对比}

\begin{enumerate}
  \item 性能

        根据上面的测试，可以得到如表 \ref{multi-test-result} 的结果。不难看出：在吞吐量上，本框架和另外二者相差不大，略逊于 Boost.Beast，和 WebSocket++ 相差无几，但都基本已经达到网络能支撑的最大带宽，瓶颈不在 CPU 上，在常见的使用场景中不会成为阻碍；在并发测试中，本框架接收连接的速度更快，从而能够更快地处理来自连接的其它请求。

        \begin{table}[ht]
          \caption{\label{multi-test-result} 各框架测试结果}
          \centering
          \begin{tabular}{|c|c|c|c|}
            \hline
                                   & 本框架 & WebSocket++ & Boost.Beast \\ \hline
            吞吐量(MB/s)           & 181.82 & 173.58      & 199.61      \\ \hline
            平均请求响应速率(个/s) & 509.9  & 165.6       & 233.6       \\ \hline
            二进制文件大小(KB)     & 183    & 425         & 659         \\ \hline
          \end{tabular}
        \end{table}

  \item 功能和完成度

        Boost.Beast 和 WebSocket++ 都是规模和本框架不在一个等级的项目。其中 Boost.Beast 包含约 65000 行代码（Boost.ASIO 相关依赖不计入），WebSocket++ 包含约 30000 行代码，而本项目仅包含约 3000 行代码，而且后二者都有过近 2000 次提交，无论从功能的完备性以及接口的成熟度而言都不具有可比性。

  \item 框架易用性

        本框架提供的接口少的同时也会带来一些好处，即开即用且接口清晰。例如上面测试中实现的示例，使用者只需要关注自身需要的核心功能；WebSocket++ 提供的默认接口也十分清晰，易于上手；Boost.Beast 提供的功能最多，抽象最为复杂，难以上手，实现一个示例服务器都需要近 200 行代码，但这也是与提供的功能丰富性之间做的权衡。

  \item 二进制文件体积

        三次测试的可执行程序均为 x64 环境下编译，二进制文件大小如表 \ref{multi-test-result} 所示。本框架仅依赖较少的外部库，使体积保持在较小的状态；WebSocket++ 和 Boost.Beast 均依赖于 Boost 库中的某些核心功能，造成一定程度的二进制文件体积膨胀也是可以理解的。

\end{enumerate}


整体而言，虽然市面上已经有许多本框架相同定位的开源产品，但本框架还是能够在某些场景中拥有一定的优势；在本框架希望解决的局域网中的医学影像传送问题中也能顺利地解决 HTTP 协议的替代问题。
